package com.base.springboot.starter.common.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 封装httpclient使用时创建和回收的公共逻辑，简化 get post的代码量，用法见测试用例
 * 多例设计，每个场景仅初始化1个httplclient即可，不要频繁创建本类实例
 * TODO 1.参数基本都是写死的，根据需求重构  注意：http 响应码不是200，程序会抛异常出来，待优化
 * User: wangwei
 * Date: 2021-01-29 2:12 下午
 */
@Slf4j
public class CaissaHttpClient {

    public static final int CONNECT_TIMEOUT = 3000;
    public static final int SOCKET_TIMEOUT = 5000;
    public static final String CONTENT_TYPE = "Content-Type";

    //thread safe
    final ObjectMapper writeMapper = new ObjectMapper();

    private static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private CloseableHttpClient httpClient;
    private PoolingHttpClientConnectionManager connectionManager;
    private CsHttpClientConfig csHttpClientConfig;

    private ScheduledThreadPoolExecutor warmSchedule = new ScheduledThreadPoolExecutor(1);
    private ThreadPoolTaskExecutor warmExecutor;

    CaissaHttpClient(String name, CsHttpClientConfig config) {

        if (config != null) {
            this.csHttpClientConfig = config;
        } else {
            this.csHttpClientConfig = new CsHttpClientConfig.Builder().build();
        }

        log.info("httpclient : {} , config : {}", name, csHttpClientConfig.toString());

        connectionManager = new PoolingHttpClientConnectionManagerBuilder().setCSConfig(csHttpClientConfig).build();

        httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .build();

        initWarmupExecutor();

        Runtime.getRuntime().addShutdownHook(
                new Thread(() -> {
                    try {
                        warmSchedule.shutdown();
                        if (warmExecutor != null) {
                            warmExecutor.shutdown();
                        }
                        httpClient.close();
                    } catch (IOException ignored) {
                    }
                })
        );
    }

    public PoolingHttpClientConnectionManager getConnectionManager() {
        return connectionManager;
    }

    /**
     * form 格式提交 连接超时3s，读超时5s
     * @param url
     * @param params
     * @param callback
     * @param <R> 返回值的序列化类型
     * @return
     * @throws Exception
     */
    public <R> R postFormRequest(String url, Map<String, Object> params, CSCallback<R> callback) throws Exception {
        return postFormRequest(url, params, null, null, callback);
    }

    /**
     * form 格式提交 指定超时时间
     * @param url
     * @param params
     * @param connectTimeoutInMillSeconds
     * @param readTimeoutInMillSeconds
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postFormRequest(String url, Map<String, Object> params, Integer connectTimeoutInMillSeconds,
                                 Integer readTimeoutInMillSeconds, CSCallback<R> callback) throws Exception {

        HttpPost httpPost = new HttpPost(encodeURL(url, DEFAULT_CHARSET.name()));
        if (params != null) {
            List<NameValuePair> pairs = params.entrySet().stream()
                    .map(e -> new BasicNameValuePair(e.getKey(), e.getValue().toString()))
                    .collect(Collectors.toList());
            httpPost.setEntity(new UrlEncodedFormEntity(pairs, DEFAULT_CHARSET.name()));
        }
        httpPost.setHeader(CONTENT_TYPE, "application/x-www-form-urlencoded;charset=" + DEFAULT_CHARSET.name());

        if (connectTimeoutInMillSeconds == null || connectTimeoutInMillSeconds.intValue() <= 0
                || readTimeoutInMillSeconds == null || readTimeoutInMillSeconds.intValue() <= 0) {
            return request0(httpPost, callback);
        }

        return request0(httpPost, getRequestConfig(connectTimeoutInMillSeconds, readTimeoutInMillSeconds), callback);
    }

    /**
     * json 格式提交
     * @param url
     * @param params 使用jackson序列化对象
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postJsonRequest(String url, Object params, CSCallback<R> callback) throws Exception {
        if (params != null) {
            return postJsonRequest(url, writeMapper.writeValueAsString(params), callback);
        }
        return postJsonRequest(url, "", callback);
    }

    /**
     * json 格式提交
     * @param url
     * @param params
     * @param connectTimeoutInMillSeconds
     * @param readTimeoutInMillSeconds
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postJsonRequest(String url, Object params,  Integer connectTimeoutInMillSeconds,
                                 Integer readTimeoutInMillSeconds, CSCallback<R> callback) throws Exception {
        String jsonReq = "";
        if (params != null) {
            jsonReq = writeMapper.writeValueAsString(params);
        }

        if (connectTimeoutInMillSeconds == null || connectTimeoutInMillSeconds.intValue() <= 0
                || readTimeoutInMillSeconds == null || readTimeoutInMillSeconds.intValue() <= 0) {
            return postJsonRequest(url, jsonReq, callback);
        }

        return postJsonRequest(url, jsonReq, connectTimeoutInMillSeconds, readTimeoutInMillSeconds, callback);
    }

    /**
     * json 格式提交 连接超时3s，读超时5s
     * @param url
     * @param jsonReq
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postJsonRequest(String url, String jsonReq, CSCallback<R> callback) throws Exception {
        return postJsonRequest(url, jsonReq, null, null, callback);
    }

    /**
     * json 格式提交 指定超时
     * @param url
     * @param jsonReq
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postJsonRequest(String url, String jsonReq, Integer connectTimeoutInMillSeconds,
                                 Integer readTimeoutInMillSeconds, CSCallback<R> callback) throws Exception {
        HttpPost httpPost = new HttpPost(encodeURL(url, DEFAULT_CHARSET.name()));
        if (!Strings.isNullOrEmpty(jsonReq)) {
            StringEntity stringEntity = new StringEntity(jsonReq, DEFAULT_CHARSET.name());
            stringEntity.setContentEncoding(DEFAULT_CHARSET.name());
            httpPost.setEntity(stringEntity);
        }
        httpPost.setHeader(CONTENT_TYPE, "application/json;charset=" + DEFAULT_CHARSET.name());

        if (connectTimeoutInMillSeconds == null || connectTimeoutInMillSeconds.intValue() <= 0
                || readTimeoutInMillSeconds == null || readTimeoutInMillSeconds.intValue() <= 0) {
            return request0(httpPost, callback);
        }

        return request0(httpPost, getRequestConfig(connectTimeoutInMillSeconds, readTimeoutInMillSeconds), callback);
    }

    /**
     * get 连接超时3s，读超时5s
     * @param url
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R get(String url, CSCallback<R> callback) throws Exception {
        return get(url, null, null, callback);
    }

    /**
     * get 请求，指定超时时间
     * @param url
     * @param connectTimeoutInMillSeconds
     * @param readTimeoutInMillSeconds
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R get(String url, Integer connectTimeoutInMillSeconds,
                     Integer readTimeoutInMillSeconds, CSCallback<R> callback) throws Exception {

        HttpGet httpGet = new HttpGet(encodeURL(url, DEFAULT_CHARSET.name()));
        if (connectTimeoutInMillSeconds == null || connectTimeoutInMillSeconds.intValue() <= 0
                || readTimeoutInMillSeconds == null || readTimeoutInMillSeconds.intValue() <= 0) {
            return request0(httpGet, callback);
        }

        return request0(httpGet, getRequestConfig(connectTimeoutInMillSeconds, readTimeoutInMillSeconds), callback);
    }

    /**
     * 通用请求方法
     * @param httpRequest
     * @param requestConfig
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R request0(HttpRequestBase httpRequest, RequestConfig requestConfig , CSCallback<R> callback) throws Exception {
        httpRequest.setConfig(requestConfig);
        httpRequest.setHeader("Accept", "*/*");

        try {
            CloseableHttpResponse response = httpClient.execute(httpRequest);
            StatusLine status = response.getStatusLine();

            int state = status.getStatusCode();
            if (state == HttpStatus.SC_OK) {
                HttpEntity responseEntity = response.getEntity();
                String entityStr = EntityUtils.toString(responseEntity, DEFAULT_CHARSET);
                return callback.success(entityStr);
            } else {
                throw new RuntimeException("response status code is expected 200,actual " + response.toString());
            }
        }finally {
            httpRequest.releaseConnection();
        }
    }

    /**
     * 通用请求方法
     * @param httpRequest
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R request0(HttpRequestBase httpRequest, CSCallback<R> callback) throws Exception {
        return request0(httpRequest, getRequestConfig(CONNECT_TIMEOUT, SOCKET_TIMEOUT), callback);
    }

    public interface CSCallback<R> {
        R success(String responseBody);
    }

    /**
     * 将URL中请求参数进行编码
     * @param url
     * @param charset
     * @return
     */
    private static String encodeURL(String url ,String charset){

        Objects.requireNonNull(url);

        if (!url.contains("?")) {
            return url;
        }

        int indexOf = url.indexOf("?");

        String uri = url.substring(0, indexOf);
        String query = url.substring(indexOf + 1);

        String[] keyAndValues = query.split("&");

        if(keyAndValues==null||keyAndValues.length==0){
            return url;
        }

        StringBuilder encodedQuery = new StringBuilder();

        int count = 0;
        for (String keyAndValue : keyAndValues) {
            count++;
            String[] arr = keyAndValue.split("=", 2);
            if (arr == null || arr.length == 0) {
                log.warn("ignore invalid param {}", keyAndValue);
                continue;
            }
            String key = arr[0];
            String value = arr[1];

            if ("null".equals(value)) {
                value = "";
            }

            if (value != null && value.length() > 0) {
                try {
                    value = URLEncoder.encode(value, charset);
                } catch (UnsupportedEncodingException e) {
                    log.warn("ignore invalid param", e);
                }
            }

            encodedQuery.append(key).append("=").append(value);

            if (count < keyAndValues.length) {
                encodedQuery.append("&");
            }
        }

        if (encodedQuery.length() > 0) {
            return encodedQuery.insert(0, "?").insert(0, uri).toString();
        }

        return uri;
    }

    public static void main(String[] args) {
        String url= CaissaHttpClient.encodeURL("https://baijiahao.baidu.com/s?id=1681364338503035787&wfr=spider&for=pc", DEFAULT_CHARSET.name());
        System.out.println(url);
    }
    public void preHeat() {
        if (this.csHttpClientConfig.getPreheatUrl() == null) {
            return;
        }

        try {
            CountDownLatch startSignal = new CountDownLatch(1);
            for (int i = 0; i < csHttpClientConfig.getMaxPreheatConn(); i++) {
                warmExecutor.execute(() -> {
                    try {
                        startSignal.await();
                        get(csHttpClientConfig.getPreheatUrl(), responseBody -> responseBody);
                    } catch (Exception ignore) {
                    }
                });
            }
            startSignal.countDown();
        } catch (Exception ignore) {
        }
    }

    private void initWarmupExecutor() {

        if (this.csHttpClientConfig.getPreheatUrl() == null) {
            return;
        }

        warmExecutor = new ThreadPoolTaskExecutor();
        warmExecutor.setCorePoolSize(csHttpClientConfig.getMaxPreheatConn());
        warmExecutor.setMaxPoolSize(csHttpClientConfig.getMaxPreheatConn());
        warmExecutor.setQueueCapacity(200);
        warmExecutor.setThreadNamePrefix("cshttpclient-warmup-");
        warmExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        warmExecutor.initialize();

        warmSchedule.scheduleAtFixedRate(() -> {
            preHeat();
        }, 1000, 500, TimeUnit.MILLISECONDS);
    }

    private RequestConfig getRequestConfig(Integer connectTimeoutInMillSeconds, Integer readTimeoutInMillSeconds) {
        return RequestConfig.custom()
                .setConnectTimeout(connectTimeoutInMillSeconds)
                .setConnectionRequestTimeout(connectTimeoutInMillSeconds)
                .setSocketTimeout(readTimeoutInMillSeconds)
                .build();
    }
}
